import {
  V1Container, V1ContainerPort, V1EnvFromSource, V1Probe,
  V1ResourceRequirements, V1VolumeDevice, V1VolumeMount
} from "@kubernetes/client-node"
import type { Logger } from 'pino'
import { URL } from "url"
import { KVString, PlanContextData } from "yam-types"
import { ContainerPortSpec, ContainerResourceQuota, DeployWorkload, ExtraVolumeMount, PodContainerSpec } from "../types"
import { BUILT_IN_ENV_APP, BUILT_IN_ENV_NAMESPACE, BUILT_IN_ENV_NODE_IP, BUILT_IN_ENV_POD_IP, BUILT_IN_ENV_PORT, BUILT_IN_ENV_STACK, BUILT_IN_ENV_TAG, BUILT_IN_ENV_VERSION, CHECK_THRESHOLD_PREVENT_TRAFFIC, CHECK_THRESHOLD_RESTART, COMMON_PORT_NAME_MAPPING, DEFAULT_CHECK_TIMEOUT, MIN_CHECK_DELAY_SECONDS } from "./constants"

export default class ContainerFactory {

  private planContextData: PlanContextData
  private workloadSpec: DeployWorkload
  private log: Logger

  constructor(planContextData: PlanContextData, workloadSpec: DeployWorkload) {
    this.planContextData = planContextData
    this.workloadSpec = workloadSpec
  }

  setLogger(logger: Logger): void {
    this.log = logger
  }

  generatePodContainers(): { initContainers: V1Container[], containers: V1Container[], containsLatest: boolean } {
    const initContainers: V1Container[] = []
    const containers: V1Container[] = []
    let idx = 0
    let idxInit = 1
    let containsLatest = false
    for (const c of this.workloadSpec.containers) {
      // 1. basic container properties
      let defaultImageNameSuffix = ''
      if (c.initContainer) {
        defaultImageNameSuffix = '-init-' + idxInit
      } else if (idx !== 0) {
        defaultImageNameSuffix = '-sidecar-' + idx
      }
      const image = c.image.indexOf(':') === -1 ? (c.image + ':' + (this.workloadSpec.version || 'latest')) : c.image
      const name = c.name || this.planContextData.app + defaultImageNameSuffix
      const isDynamicLatest = image.endsWith('latest')

      if (isDynamicLatest) {
        containsLatest = true
      }

      const imagePullPolicy = c.imagePullPolicy ? c.imagePullPolicy : (isDynamicLatest ? "Always" : "IfNotPresent")
      const command = typeof c.command === "string" ? ['sh', '-c', '--', c.command] : c.command

      const tmpContainer: V1Container = {
        // yam-engine built-in fields
        name,
        image,
        command,
        args: c.args,
        env: c.env || [],
        imagePullPolicy,
        workingDir: c.workingDir,

        // compatible with native kubernetes API
        startupProbe: c.startupProbe as V1Probe,
        stdin: c.stdin as boolean,
        stdinOnce: c.stdinOnce as boolean,
        tty: c.tty as boolean,
        envFrom: c.envFrom as V1EnvFromSource[],
        securityContext: Object.assign(c.privileged ? { privileged: true } : {}, c.securityContext),
        terminationMessagePath: c.terminationMessagePath as string,
        terminationMessagePolicy: c.terminationMessagePolicy as string,
        volumeDevices: c.volumeDevices as V1VolumeDevice[]
      }

      // 2. calculate resources quota
      if (c.quotas) {
        tmpContainer.resources = this.generateResourceQuota(c.quotas)
      }
      if (c.resources) {
        tmpContainer.resources = Object.assign({}, c.resources, tmpContainer.resources) as V1ResourceRequirements
      }

      // 3. container liveness & readiness probes
      const { liveness, readiness } = this.generateContainerProbes(c)
      tmpContainer.livenessProbe = liveness
      tmpContainer.readinessProbe = readiness

      // 4. build container ports/hostPorts
      if (c.port) {
        tmpContainer.ports = ContainerFactory.generatePortSpec(c.port)
      }

      // 5. lifecycle hooks
      this.generateLifecycleHooks(tmpContainer, c)

      // 6. add volumeMounts
      tmpContainer.volumeMounts = this.generateContainerVolumeMounts(c, this.workloadSpec.mounts)

      // 7. inject fieldRef and metadata
      this.injectFieldRefsToEnv(tmpContainer)

      // 8. add container to corresponding list
      c.initContainer ? initContainers.push(tmpContainer) : containers.push(tmpContainer)
      c.initContainer ? idxInit++ : idx++
    }

    return { initContainers, containers, containsLatest }
  }

  generateResourceQuota(r: ContainerResourceQuota): V1ResourceRequirements | undefined {
    const quota = {
      requests: {} as KVString,
      limits: {} as KVString
    }
    let cpuMultiplier = 1
    let memoryMultiplier = 1
    if (r.burstMultiplier?.cpu) {
      cpuMultiplier = r.burstMultiplier.cpu
    }
    if (r.burstMultiplier?.memory) {
      memoryMultiplier = r.burstMultiplier.memory
    }
    if (r.cpu) {
      const result = this.extractRequestsAndLimits(r.cpu, cpuMultiplier)
      quota.requests.cpu = result.requests
      quota.limits.cpu = result.limits
    }
    if (r.memory) {
      const result = this.extractRequestsAndLimits(r.memory, memoryMultiplier)
      quota.requests.memory = result.requests
      quota.limits.memory = result.limits
    }
    // GPU doesn't support burst feature currently
    if (r.gpu) {
      if (r.gpuType === "amd") {
        quota.limits["amd.com/gpu"] = r.gpu.toString()
      } else {
        // defaults to widely used nvidia
        quota.limits["nvidia.com/gpu"] = r.gpu.toString()
      }
    }
    return quota
  }

  extractRequestsAndLimits(raw: string | number, multiplier: number): { requests: string, limits: string } {
    const m = raw.toString().trim()
    const tmp = m.split(/\d+/)
    const suffix = tmp[tmp.length - 1]
    const limits = (parseInt(m) * multiplier) + suffix
    this.log.debug(`convert raw resources quotas from '${raw}' to { requests: ${m}, limits: ${limits}}`)
    if (limits.indexOf('NaN') !== -1) {
      this.log.error(`converting quotas data failed, please check 'quotas' field or use native 'resources' field instead.`)
    }
    return { requests: m, limits }
  }

  generateContainerProbes(c: PodContainerSpec): { liveness?: V1Probe, readiness?: V1Probe } {
    const result = { liveness: c.livenessProbe as V1Probe, readiness: c.readinessProbe as V1Probe }

    if (c.healthCheck && c.healthCheck.enable) {
      const check = c.healthCheck
      const initialDelay = check.initialDelaySeconds || this.workloadSpec.minReadySeconds || MIN_CHECK_DELAY_SECONDS
      const probe: V1Probe = {
        periodSeconds: check.intervalSeconds,
        timeoutSeconds: (check.timeoutSeconds as number) || DEFAULT_CHECK_TIMEOUT,
        initialDelaySeconds: initialDelay
      }
      if (check.command) {
        probe.exec = {
          command: ["sh", "-c", "--", check.command]
        }
      } else if (check.url) {
        // must be complete url, etc. http://127.0.0.1/health
        const url = new URL(check.url)
        const port = new Object(parseInt(url.port || '80'))
        const host = url.host
        if (url.protocol.startsWith('tcp')) {
          probe.tcpSocket = { port, host }
        } else {
          probe.httpGet = {
            host, port,
            path: url.pathname,
            // kubernetes will skip cert validation and send request if it's https
            scheme: url.protocol.startsWith('https') ? 'HTTPS' : 'HTTP'
          }
        }
      }
      check.onFailure?.forEach(onFail => {
        if (onFail.action === "preventTraffic") {
          // readiness probe
          result.readiness = Object.assign({
            failureThreshold: CHECK_THRESHOLD_PREVENT_TRAFFIC
          }, probe)
        } else if (onFail.action === "restart") {
          // liveness probe
          result.liveness = Object.assign({
            failureThreshold: CHECK_THRESHOLD_RESTART
          }, probe)
        }
      })
    }
    return result
  }

  static generatePortSpec(port: string | number | ContainerPortSpec[]): V1ContainerPort[] {
    if (typeof port === "number") {
      return [{
        protocol: 'TCP',
        name: 'tcp-' + port,
        containerPort: port
      }]
    } else if (typeof port === "string") {
      const portNum = parseInt(port)
      if (isNaN(portNum)) {
        if (!COMMON_PORT_NAME_MAPPING[port]) {
          throw new Error(`can not resolve port ${port}, not recognized`)
        }
        return [{
          protocol: 'TCP',
          name: port,
          containerPort: COMMON_PORT_NAME_MAPPING[port]
        }]
      } else {
        return this.generatePortSpec(portNum)
      }
    } else {
      return port.map(p => {
        return {
          protocol: p.protocol,
          name: p.name,
          containerPort: p.containerPort,
          hostPort: p.hostPort,
          hostIP: p.hostIP as string
        }
      })
    }
  }

  generateLifecycleHooks(tmpContainer: V1Container, c: PodContainerSpec): void {
    if (c.lifecycleHook) {
      tmpContainer.lifecycle = {}
      if (c.lifecycleHook.afterStart) {
        tmpContainer.lifecycle.postStart = {
          exec: {
            command: c.lifecycleHook.afterStart
          }
        }
      }
      if (c.lifecycleHook.beforeTerminate) {
        tmpContainer.lifecycle.preStop = {
          exec: {
            command: c.lifecycleHook.beforeTerminate
          }
        }
      }
    }
  }

  generateContainerVolumeMounts(c: PodContainerSpec, mounts?: ExtraVolumeMount[]): V1VolumeMount[] {
    const containerName = c.name
    const vMounts: V1VolumeMount[] = c.volumeMounts ? (c.volumeMounts as V1VolumeMount[]) : []
    mounts?.forEach(m => {
      if (m.container && !containerName) {
        return
      }
      if (m.container && containerName) {
        // check if matches containerName, if not, do not mount to that container
        if (typeof m.container === "string" && m.container !== containerName) {
          return
        }
        if (typeof m.container === "object" && !m.container.includes(containerName)) {
          return
        }
      }
      vMounts.push({
        mountPath: m.path,
        name: m.name,
        readOnly: m.readOnly,

        // compatible with kubernetes native API
        subPath: m.subPath as string,
        subPathExpr: m.subPathExpr as string,
        mountPropagation: m.mountPropagation as string,
      })
    })
    return vMounts
  }

  injectFieldRefsToEnv(c: V1Container): void {
    // inject application metadata and version
    c.env?.push({
      name: BUILT_IN_ENV_APP,
      value: this.planContextData.app
    })
    c.env?.push({
      name: BUILT_IN_ENV_NAMESPACE,
      value: this.planContextData.namespace
    })
    c.env?.push({
      name: BUILT_IN_ENV_VERSION,
      value: this.workloadSpec.version
    })
    c.env?.push({
      name: BUILT_IN_ENV_TAG,
      value: this.planContextData.envTagName
    })
    c.env?.push({
      name: BUILT_IN_ENV_STACK,
      value: this.planContextData.stackName
    })
    // inject pod/node IP
    c.env?.push({
      name: BUILT_IN_ENV_POD_IP,
      valueFrom: {
        fieldRef: {
          fieldPath: "status.podIP"
        }
      }
    })
    c.env?.push({
      name: BUILT_IN_ENV_NODE_IP,
      valueFrom: {
        fieldRef: {
          fieldPath: "status.hostIP"
        }
      }
    })
    // inject SERVER_PORT env if primary port exists
    if (c.ports && c.ports[0]?.containerPort) {
      c.env?.push({
        name: BUILT_IN_ENV_PORT,
        value: c.ports[0]?.containerPort + ''
      })
    }
  }
}